/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using DaveSexton.DocProject.Sandcastle.Configuration;
using System.Xml;
using System.Text.RegularExpressions;
using DaveSexton.DocProject.Sandcastle.Resources;
using DaveSexton.Controls;
using System.Xml.XPath;

namespace DaveSexton.DocProject.Sandcastle.ContentManagement
{
	[Serializable]
	public class XmlContentDocument : ContentDocument
	{
		#region Public Properties
		public XmlNamespaceManager Namespaces
		{
			get
			{
				return namespaces;
			}
		}

		public IEnumerable<string> CustomItems
		{
			get
			{
				return customItems.AsReadOnly();
			}
		}
		#endregion

		#region Private / Protected
		[NonSerialized]
		private readonly XmlNamespaceManager namespaces = new XmlNamespaceManager(new NameTable());
		private readonly List<string> designerItems, customItems;
		private readonly string itemsXPath;
		[NonSerialized]
		private XPathExpression itemsXPathExpr;
		[NonSerialized]
		private XmlDocument document, committedDocument;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="XmlContentDocument" /> class.
		/// </summary>
		public XmlContentDocument(string name)
			: base(name)
		{
			customItems = new List<string>(0);
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="XmlContentDocument" /> class.
		/// </summary>
		public XmlContentDocument(string name, IEnumerable<string> customItems)
			: base(name)
		{
			if (customItems == null)
				this.customItems = new List<string>(0);
			else
			{
				this.customItems = new List<string>(customItems);
				this.customItems.Sort(StringComparer.OrdinalIgnoreCase);
			}
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="XmlContentDocument" /> class.
		/// </summary>
		public XmlContentDocument(string file, string itemsXPath, IEnumerable<string> designerItems)
			: base(file)
		{
			if (string.IsNullOrEmpty(itemsXPath))
				throw new ArgumentException(Errors.NullOrEmpty, "itemsXPath");

			customItems = new List<string>(0);

			this.itemsXPath = itemsXPath;

			if (designerItems == null)
				this.designerItems = new List<string>(0);
			else
			{
				this.designerItems = new List<string>(designerItems);
				this.designerItems.Sort(StringComparer.OrdinalIgnoreCase);
			}
		}
		#endregion

		#region Methods
		public override ContentItem CreateItem(string name)
		{
			throw new NotSupportedException();
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")]
		private static void AddNamespace(XmlNamespaceManager namespaces, XmlReader reader)
		{
			string uri = reader.NamespaceURI;

			if (!string.IsNullOrEmpty(uri))
			{
				string prefix = reader.Prefix;

				if (!string.Equals(prefix, "xml", StringComparison.Ordinal))
				{
					if (string.Equals(prefix, "xmlns", StringComparison.Ordinal))
					{
						prefix = reader.LocalName;
						uri = reader.Value;
					}

					if (!namespaces.HasNamespace(prefix))
						namespaces.AddNamespace(prefix, uri);

					// HACK: An IHTMLDocument2 will convert all unknown prefixes to lower case; therefore, 
					// a lowered version of each must be added as well.  Obviously this presents a problem 
					// when there are two identical prefixes with different namespaces, without regard to case;
					// however, there doesn't appear to be anything like that in the Sandcastle content item
					// documents or in typical XML documentation comment files, which is what this document
					// handles.
					string lowered = prefix.ToLowerInvariant();

					if (!namespaces.HasNamespace(lowered))
						namespaces.AddNamespace(lowered, uri);
				}
			}
		}

		private void FillNamespaces()
		{
			using (XmlReader reader = document.CreateNavigator().ReadSubtree())
			{
				while (reader.Read())
				{
					if (reader.NodeType == XmlNodeType.Element)
					{
						AddNamespace(namespaces, reader);

						if (reader.HasAttributes)
						{
							while (reader.MoveToNextAttribute())
								AddNamespace(namespaces, reader);

							reader.MoveToElement();
						}
					}
				}
			}
		}

		[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
		public sealed override void Load(DaveSexton.Controls.Importer rebaseTargetDirectory)
		{
			throw new NotSupportedException();
		}

		public virtual void Load(DaveSexton.Controls.Importer rebaseTargetDirectory, HtmlToXmlConverterCallback converter)
		{
			if (committedDocument != null)
				document = (XmlDocument) committedDocument.Clone();
			else
			{
				document = new XmlDocument();
				document.PreserveWhitespace = true;

				document.Load(FilePath);

				FillNamespaces();
			}

			if (itemsXPathExpr == null)
				itemsXPathExpr = XPathExpression.Compile(itemsXPath, namespaces);

			Items.Clear();

			foreach (XPathNavigator navigator in document.CreateNavigator().Select(itemsXPathExpr))
			{
				string name = navigator.GetAttribute("id", "");

				if (!string.IsNullOrEmpty(name))
				{
					bool design = designerItems.BinarySearch(name, StringComparer.OrdinalIgnoreCase) > -1
						|| navigator.SelectChildren(XPathNodeType.Element).Count > 0;

					ContentItem item;

					if (string.Equals(name, "header", StringComparison.Ordinal) || string.Equals(name, "footer", StringComparison.Ordinal))
						item = new DivContentItem(navigator, name, design);
					else
						item = new XmlContentItem(navigator, name, design);

					Items.Add(item);

					if (design && rebaseTargetDirectory != null)
					{
						string value = item.Value;

						if (rebaseTargetDirectory.RebaseImagesToTargetDirectory(ref value) > 0)
						{
							if (converter != null)
							{
								try
								{
									value = converter(value, namespaces);
								}
								catch (ArgumentException ex)
								{
									value = "<font color=\"red\">" + HtmlToXmlConverter.Encode(ex.Message) + "</font>" + Environment.NewLine +
										"<!-- ORIGINAL VALUE" + Environment.NewLine + HtmlToXmlConverter.Encode(value) + Environment.NewLine + "-->";
								}
							}

							item.Value = value;
							item.IsDirty = false;
						}
					}
				}
			}
		}

		public override void Commit()
		{
			if (document == null)
				throw new InvalidOperationException(Errors.XmlContentDocumentNotLoaded);

			committedDocument = (XmlDocument) document.Clone();
		}

		public override void Cancel()
		{
			if (committedDocument != null)
			{
				document = committedDocument;
				committedDocument = null;
			}
		}

		public override void Save()
		{
			if (IsDirty)
			{
				if (committedDocument != null)
					// NOTE: committedDocument should not be nulled since it is used again in the Load method if it's not null
					committedDocument.Save(FilePath);
				else if (document != null)
					document.Save(FilePath);
				else
					throw new InvalidOperationException(Errors.XmlContentDocumentNotLoaded);

				IsDirty = false;

				foreach (ContentItem item in Items)
					item.IsDirty = false;
			}
		}

		public override void Save(string targetPath)
		{
			if (string.IsNullOrEmpty(targetPath))
				throw new ArgumentException(Errors.NullOrEmpty, "targetPath");

			if (IsDirty)
			{
				string path = System.IO.Path.Combine(targetPath, System.IO.Path.GetFileName(FilePath));

				if (committedDocument != null)
					committedDocument.Save(path);
				else if (document != null)
					// if the document's changes were never committed then the document itself should be saved
					document.Save(path);
				else
					throw new InvalidOperationException(Errors.XmlContentDocumentNotLoaded);

				// NOTE: committedDocument should not be nulled since it is used again in the Load method, if it's not null
				IsDirty = false;
			}
		}
		#endregion
	}
}
